import pygame as pg
import sys,os,pickle

class _Collision(object):
    """Pulling some of the collision detection methods out for better organization.
       Inherited by Player (and possibly other sprites later)."""
    def detect_ground(self,Level):
        """Calls the appropriate collision function depending on if the player is
        on the ground or in the air."""
        if not self.fall:
            self.grounded(Level)
        else:
            self.airborne(Level)
        self.reset_wall_floor_rects()

    def grounded(self,Level):
        """Detects the ground beneath the player when not jumping. Implementation
        is based around the information found at:
        http://info.sonicretro.org/Sonic_Physics_Guide"""
        change = None
        pads_hit = [False,False]
        for i,floor_detect in enumerate(self.floor_detect_rects):
            collide,pads_hit = self.check_floor_detector_initial(pads_hit,(i,floor_detect),Level)
            if collide:
                change = self.check_floor_detector_detail(collide,(i,floor_detect),change,Level)
        if pads_hit[0]^pads_hit[1]:
            change = self.detect_glitch_fix(pads_hit,change,Level)
        if change != None:
            self.rect.y = int(change-self.rect.height)
        else:
            self.fall = True

    def check_floor_detector_initial(self,pads_hit,pad_details,Level):
        """Find out if a detector is hitting a solid cell."""
        i,floor_detect = pad_details
        collide = []
        for cell in Level.rect_dict:
            if floor_detect.colliderect(Level.rect_dict[cell]):
                collide.append(cell)
                pads_hit[i] = True
        return collide,pads_hit

    def check_floor_detector_detail(self,collide,pad_details,change,Level):
        """Get exact ground value from a colliding detector."""
        i,floor_detect = pad_details
        for key in collide:
            coord = Level.map_dict[key]
            cell_heights = Level.height_dict[coord]
            x_loc_in_cell = floor_detect.x-key[0]*Level.cell_size[0]
            offset = cell_heights[x_loc_in_cell]
            if change == None:
                change = (key[1]+1)*Level.cell_size[1]-offset
            else:
                change = min((key[1]+1)*Level.cell_size[1]-offset,change)
        return change

    def detect_glitch_fix(self,pads,change,Level):
        """Fixes a glitch with the blit location that occurs on up-slopes when
        one detection bar hits a solid cell and the other doesn't. This could
        probably still be improved."""
        inc,index = ((1,0) if not pads[0] else (-1,1))
        detector = self.floor_detect_rects[index].copy()
        pad_details = (index,detector)
        old_change = change
        while detector.x != self.floor_detect_rects[not index].x:
            detector.x += inc
            collide = self.check_floor_detector_initial([0,0],pad_details,Level)[0]
            change = self.check_floor_detector_detail(collide,pad_details,change,Level)
            if change < old_change:
                return change
        return old_change

    def airborne(self,Level):
        """Search for the ground via mask detection while in the air."""
        mask = self.floor_detect_mask
        check_rects = (pg.Rect(self.rect.x+1,self.rect.y,self.rect.width-1,1),
                       pg.Rect(self.rect.x+1,self.rect.bottom-1,self.rect.width-2,1))
        stop_fall = False
        for rect in check_rects:
            if self.collide_with(Level,rect,mask,[0,int(self.y_vel)]):
                self.y_vel = self.adjust_pos(Level,rect,mask,[0,int(self.y_vel)],1,)
                stop_fall = True
        self.rect.y += int(self.y_vel)
        if stop_fall:
            self.fall = False

    def detect_wall(self,Level):
        """Detects collisions with walls."""
        if not self.fall:
            rect,mask = self.wall_detect_rect,self.wall_detect_mask
        else:
            rect,mask = self.rect,self.fat_mask
        if self.collide_with(Level,rect,mask,(int(self.x_vel),0)):
            self.x_vel = self.adjust_pos(Level,rect,mask,[int(self.x_vel),0],0)
        self.rect.x += int(self.x_vel)
        self.reset_wall_floor_rects()

    def adjust_pos(self,Level,rect,mask,offset,off_ind):
        """Continuously calls the collide_with method, decrementing the players
        rect position until no collision is detected."""
        offset[off_ind] += (1 if offset[off_ind]<0 else -1)
        while 1:
            if any(self.collide_with(Level,rect,mask,offset)):
                offset[off_ind] += (1 if offset[off_ind]<0 else -1)
                if not offset[off_ind]:
                    return 0
            else:
                return offset[off_ind]

    def collide_with(self,Level,rect,mask,offset):
        """The real collision detection occurs here. Initial tests are done with
        rect collision and if positive further tests are done with masks."""
        test = pg.Rect((rect.x+offset[0],rect.y+offset[1]),rect.size)
        self.collide_ls = []

        for cell,rec in Level.rect_dict.items(): #Rect collision first
            if test.colliderect(rec):  #If rect collision positive, test masks.
                mask_test = test.x-Level.rect_dict[cell].x,test.y-Level.rect_dict[cell].y
                if Level.mask_dict[Level.map_dict[cell]].overlap_area(mask,mask_test):
                    self.collide_ls.append(cell)
        return self.collide_ls

class Player(_Collision):
    def __init__(self,loc,size):
        self.x_vel = self.y_vel = 0
        self.fall = False
        self.speed = 3
        self.jump_power = -6.5
        self.jump_cut = -4
        self.grav = 0.22

        self.rect = pg.Rect(loc,size)
        self.image = pg.Surface((self.rect.size)).convert()
        self.image.fill((100,0,255))

        self.setup_collision_rects()

    def phys_update(self):
        """Currently just a very basic gravity function."""
        if self.fall:
            self.y_vel += self.grav
        else:
            self.y_vel = 0

    def setup_collision_rects(self):
        """setup for the collision detection bars and rects. Currently only run on init."""
        self.reset_wall_floor_rects()
        self.fat_mask = pg.Mask(self.rect.size)
        self.fat_mask.fill()
        self.wall_detect_mask = pg.Mask(self.wall_detect_rect.size)
        self.wall_detect_mask.fill()
        self.floor_detect_mask = pg.Mask((self.rect.width-2,1))
        self.floor_detect_mask.fill()
        self.collide_ls = []

    def reset_wall_floor_rects(self):
        """Sets the collision detection bar rects based on the players rect position.
        Called every frame."""
        self.floor_detect_rects = (pg.Rect((self.rect.x+1,self.rect.y),(1,self.rect.height+16)),
                                   pg.Rect((self.rect.right-2,self.rect.y),(1,self.rect.height+16)))
        self.wall_detect_rect = pg.Rect(self.rect.x,self.rect.bottom-10,self.rect.width,1)

    def update(self,Level,Surf):
        keys = pg.key.get_pressed()
        self.x_vel = 0
        if keys[pg.K_RIGHT]:
            self.x_vel += self.speed
        if keys[pg.K_LEFT]:
            self.x_vel -= self.speed

        self.detect_wall(Level)
        self.detect_ground(Level)
        self.phys_update()

        Surf.blit(self.image,self.rect)
        for wreck in self.floor_detect_rects:
            Surf.fill((255,0,0),wreck) #Draw floor detection bar
        Surf.fill((0,255,255),self.wall_detect_rect) #Draw wall detection bar

class LevelMap:
    def __init__(self,sheet):
        self.pallet = sheet
        self.cell_size = (32,32)
        self.map_rect = pg.Rect(0,0,544,544)
        self.map_dict = self.load_map()
        self.rect_dict = self.make_rect_dict()

        self.cells = rip_from_sheet(SHEET,self.cell_size,(8,2))
        self.height_dict = gen_height_map(self.cells)
        self.mask_dict = self.make_mask_dict()

    def load_map(self):
        with open("mymap.txt","rb") as myfile:
            loaded_map = pickle.load(myfile)
            return loaded_map

    def make_rect_dict(self):
        w,h = self.cell_size
        return {cell:pg.Rect(cell[0]*w,cell[1]*h,w,h) for cell in self.map_dict}

    def make_mask_dict(self):
        return {cell:pg.mask.from_surface(self.cells[cell]) for cell in self.cells}

    def update(self,Surf):
        for destin,target in self.map_dict.items():
            destin = (destin[0]*self.cell_size[0]+self.map_rect.x,
                      destin[1]*self.cell_size[1]+self.map_rect.y)
            Surf.blit(self.cells[target],destin)

class Game:
    def __init__(self):
        self.screen = pg.display.get_surface()
        self.Clock = pg.time.Clock()
        self.done = False

        self.MyPlayer = Player((35,25),(32,15))
        self.MyMap = LevelMap(SHEET)

    def event_loop(self):
        keys = pg.key.get_pressed()
        for event in pg.event.get():
            if event.type == pg.QUIT or keys[pg.K_ESCAPE]:
                self.done = True
            elif event.type == pg.KEYDOWN: #Key down events.
                if event.key == pg.K_SPACE and not self.MyPlayer.fall: #Jump
                    self.MyPlayer.y_vel = self.MyPlayer.jump_power
                    self.MyPlayer.fall = True
            elif event.type == pg.KEYUP:
                if event.key == pg.K_SPACE and self.MyPlayer.fall: #Jump
                    if self.MyPlayer.y_vel < self.MyPlayer.jump_cut:
                        self.MyPlayer.y_vel = self.MyPlayer.jump_cut

    def main(self):
        while not self.done:
            self.event_loop()
            self.screen.fill(0)
            self.MyMap.update(self.screen)
            self.MyPlayer.update(self.MyMap,self.screen)
            pg.display.update()
            self.Clock.tick(60)

####
def gen_height_map(cells):
    height_dict = {}
    test_mask = pg.Mask((1,cells[(0,0)].get_height()))
    test_mask.fill()
    for coord,cell in cells.items():
        heights = []
        mask = pg.mask.from_surface(cell)
        for i in range(cell.get_width()):
            height = mask.overlap_area(test_mask,(i,0))
            heights.append(height)
        height_dict[(coord)] = heights
    return height_dict

def rip_from_sheet(sheet,cell_size,sheet_size):
    coord_dict = {}
    for j in range(sheet_size[1]):
        for i in range(sheet_size[0]):
            rect = pg.Rect((i*cell_size[0],j*cell_size[1]),cell_size)
            coord_dict[(i,j)] = sheet.subsurface(rect)
    return coord_dict

if __name__ == "__main__":
    os.environ['SDL_VIDEO_CENTERED'] = '1' #Center window.
    pg.init()
    pg.display.set_mode((544,256))
    SHEET = pg.image.load("basic_tileset_big.png").convert()
    SHEET.set_colorkey((255,0,255))

    MyGame = Game()
    MyGame.main()
    pg.quit();sys.exit()




